import { beforeEach, describe, expect, it, vi } from "vitest";
import { decodeMaskOnDisk } from "./mask-decoder";

// mock the entire canvas-decoder module,
// it's hard to test it because it uses offline canvas and createImageBitmap
vi.mock("./canvas-decoder", () => {
  return {
    decodeWithCanvas: vi.fn().mockResolvedValue({
      // Return some fake decoded info
      buffer: new ArrayBuffer(4),
      channels: 4,
      arrayType: "Uint8Array",
      shape: [2, 2],
    }),
  };
});

/**
 * This is a minimal 3x3 16-bit grayscale PNG.
 * Generated by following python code:
import numpy as np
from PIL import Image
import base64
from io import BytesIO

# 3x3 grayscale image with 16-bit depth (values between 0 and 65535)
image_array = np.array([
    [0, 32768, 65535],
    [16384, 49152, 32768],
    [65535, 0, 16384]
], dtype=np.uint16)

image = Image.fromarray(image_array, mode='I;16')

buffer = BytesIO()
image.save(buffer, format="PNG")

base64_string = base64.b64encode(buffer.getvalue()).decode("utf-8")
print(base64_string)
 */
const createReal16BitPngBlob = () => {
  const BASE64_16_BIT_GRAY_3x3 =
    "iVBORw0KGgoAAAANSUhEUgAAAAMAAAADEAAAAAAj0zYgAAAAG0lEQVR4nAXBAQEAAAgCIKY5zWk9M0Bt4pSNeENrBj2oAHn6AAAAAElFTkSuQmCC";

  const binary = atob(BASE64_16_BIT_GRAY_3x3);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }

  // need to mock these methods for the Blob-like object
  // because it's not available in testing environment
  return {
    type: "image/png",
    async arrayBuffer() {
      return bytes.buffer;
    },
    slice(start: number, end: number) {
      const slicedBytes = bytes.slice(start, end);
      return {
        async arrayBuffer() {
          return slicedBytes.buffer;
        },
      };
    },
  } as unknown as Blob;
};

/**
 * A helper to create a Blob-like object with given data & type.
 * The mask-decoder code calls `blob.slice(0, 27).arrayBuffer()` to read the header,
 * so we need to implement slice() in a minimal way for testing.
 */
const createFakeBlob = (data: Uint8Array, type = "image/png") => {
  return {
    type,
    slice(start: number, end: number) {
      return {
        async arrayBuffer() {
          return data.slice(start, end).buffer;
        },
      };
    },
    async arrayBuffer() {
      return data.buffer;
    },
  } as unknown as Blob;
};

import { decodeWithCanvas } from "./canvas-decoder";

describe("decodeMaskOnDisk tests", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("should call decodeWithCanvas if the file is not a PNG", async () => {
    // just pass some random bytes; type is not image/png
    const fakeData = new Uint8Array([1, 2, 3, 4, 5]);
    const blob = createFakeBlob(fakeData, "image/jpeg");

    const result = await decodeMaskOnDisk(blob, "some-class");

    expect(decodeWithCanvas).toHaveBeenCalledTimes(1);
    expect(result.buffer).toBeInstanceOf(ArrayBuffer);
  });

  it("should call customDecode16BitPng for a real 16-bit PNG", async () => {
    const blob = createReal16BitPngBlob();

    const result = await decodeMaskOnDisk(blob, "16bit-real");

    expect(decodeWithCanvas).not.toHaveBeenCalled();

    // result should come from fast-png's decode. check a few properties:
    expect(result).toBeTruthy();
    expect(result.shape).toEqual([3, 3]);
    expect(result.channels).toEqual(1);
    expect(result.arrayType).toEqual("Uint16Array");
  });

  it("should call decodeWithCanvas if it's an 8-bit PNG", async () => {
    // Minimal 8-bit PNG header
    const header = new Uint8Array(27);
    // PNG signature
    header.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0);
    // bitDepth = 8
    header[24] = 8;
    // colorType = 2 => Truecolor
    header[25] = 2;

    const blob = createFakeBlob(header, "image/png");

    const result = await decodeMaskOnDisk(blob, "8bit-rgb");

    expect(decodeWithCanvas).toHaveBeenCalledTimes(1);
    // not a 16-bit PNG => customDecode16BitPng should not be called
    expect(result).toBeTruthy();
  });
});
